React 18 + Node.js PostgreSQL + Redis Docker + GitHub Actions AWS EC2 + Nginx 14 Errors Solved
https://hrerp.infikuber.com
HR Admin & Payroll ERP Β· Multi-tenant SaaS Β· Production since March 2026
~3 min
Deploy time (git push β†’ live)
πŸ“ Complete Architecture β€” Developer PC β†’ GitHub β†’ Docker Hub β†’ AWS EC2 β†’ User
Development & Source
Developer PC
React + Node.js code
β†’ git push β†’
GitHub Repo
hr-admin-payroll-erp
β†’ triggers β†’
GitHub Actions
CI/CD Workflows
Build & Deploy
Lint + Test
ESLint, Jest, Trivy
β†’
Docker Build
Backend + Frontend images
β†’
Docker Hub
your-dockerhub/hr-erp-*
β†’ SSH pull β†’
AWS EC2
t3.medium Ubuntu 22.04
Production Stack on EC2
Nginx
Port 80/443 Β· Reverse Proxy
β†’
React Frontend
hr_erp_ui Β· Port 3000
Node.js API
hr_erp_api Β· Port 5000
PostgreSQL 15
hr_erp_db Β· Port 5432
Redis 7
hr_erp_redis Β· Port 6379
User Access
User Browser
Chrome / Mobile
β†’ HTTPS β†’
hrerp.infikuber.com
SSL via Let's Encrypt
β†’
Hostinger DNS
A record β†’ YOUR_EC2_IP
πŸ“… Development Timeline β€” 9 Steps from Code to Production
1
Code Preparation β€” Local PC
React 18 + Tailwind CSS frontend. Node.js + Express backend. PostgreSQL schema designed. Docker + docker-compose configured. All routes, middleware, services written.
2
GitHub Repository Setup
Created private repo hr-admin-payroll-erp. 5 GitHub Actions workflows added: ci-cd.yml, pr-checks.yml, security-scan.yml, rollback.yml, backup.yml. Dependabot configured for auto dependency updates.
3
AWS EC2 Instance Launch
AWS Console β†’ EC2 β†’ t3.medium (2 vCPU, 4GB RAM) Β· Ubuntu 22.04 LTS Β· 25GB gp3 storage. Security group: SSH port 22 (my IP only), HTTP 80 + HTTPS 443 (anywhere). Ports 3000/5000 NOT public β€” Docker binds to 127.0.0.1 only.
4
EC2 Server Configuration
Installed: Docker Engine, Nginx (reverse proxy), Certbot (SSL), UFW firewall. Generated SSH key for GitHub access. Created .env file with all secrets. Created systemd service for auto-start on reboot.
5
GitHub Actions CI/CD Setup
8 GitHub Secrets configured: DOCKERHUB_USERNAME, DOCKERHUB_TOKEN, EC2_HOST, EC2_SSH_KEY, DB_PASSWORD, REDIS_PASSWORD, JWT_SECRET, REFRESH_TOKEN_SECRET. Full pipeline: Lint β†’ Security β†’ Test β†’ Docker Build β†’ Deploy β†’ Smoke Test (~3 min total).
6
Docker Containers Setup
4 containers in hr_net network: PostgreSQL 15, Redis 7-alpine, Node.js backend, React frontend (served via Nginx). All ports bound to 127.0.0.1. Healthchecks on all services. depends_on chaining: postgres+redis β†’ backend β†’ frontend.
7
Domain DNS Configuration
Domain: infikuber.com (Hostinger). Added A record: hrerp β†’ YOUR_EC2_IP. Deleted old Hostinger A and AAAA records that were pointing to wrong IPs. Waited 5-10 min for DNS propagation.
8
SSL Certificate β€” HTTPS
Certbot auto-issued Let's Encrypt certificate. Auto-configured Nginx for HTTP→HTTPS redirect. Certificate saved at /etc/letsencrypt/live/hrerp.infikuber.com/. Expires 2026-06-09, auto-renewal scheduled.
9
πŸŽ‰ LIVE β€” https://hrerp.infikuber.com
All 4 containers healthy. GitHub Actions all green. Every git push auto-deploys in ~3 minutes. Health check at /health endpoint confirmed working.
Frontend Stack
βš›οΈ
React
v18.2.0
UI framework. JSX components. react-router-dom v6 for routing. react-hot-toast for notifications.
Frontend
🎨
Tailwind CSS
v3.4.0
Utility-first CSS. All UI styling. Responsive design. Custom config.
Styling
πŸ“Š
Recharts
v2.10.1
Analytics charts. Payroll trend, headcount bar charts, attendance gauge.
Charts
πŸ”—
Axios
v1.6.2
HTTP client. All API calls to backend. Interceptors for JWT headers.
HTTP
Backend Stack
🟒
Node.js
v20 LTS
JavaScript runtime. Non-blocking I/O. Required β‰₯18.0.0 per package.json engines field.
Runtime
πŸš‚
Express
v4.18.2
Web framework. 25 route files. Middleware chain: helmet β†’ cors β†’ rate-limit β†’ auth β†’ handler.
Framework
πŸ”
JWT + bcryptjs
jwt 9.0.2
Authentication. JWT 8h access + 7d refresh tokens. bcrypt for password hashing. Role-based middleware.
Auth
πŸ“§
Nodemailer + PDFKit
nodemailer 6.9.7
SMTP email dispatch. PDFKit generates payslip PDFs. Multer for file uploads (receipts, photos).
Services
Database & Infrastructure
🐘
PostgreSQL
v15-alpine
Primary database. UUID primary keys. Multi-tenant with tenant_id on every table. 13 migration phases.
Database
πŸ”΄
Redis
v7-alpine
Caching layer. Session management. 256MB max memory. LRU eviction policy. Persistent with AOF.
Cache
🐳
Docker + Compose
Docker v24+
4 containers in hr_net network. Multi-stage frontend build. Non-root user for security. Health checks on all.
Container
🌐
Nginx
1.25-alpine
Reverse proxy. Serves React build. Proxies /api → Node.js. SSL termination. HTTP→HTTPS redirect.
Proxy
Backend Dependencies β€” Full List
PackageVersionPurposeUsed In
express^4.18.2Web framework, routingserver.js, all routes
pg + pg-pool^8.11.3PostgreSQL client + connection pooldatabase.js
redis^4.6.11Redis client for cachingrateLimiter, sessions
jsonwebtoken^9.0.2JWT sign/verify for authauth.js middleware
bcryptjs^2.4.3Password hashingauth routes
helmet^7.1.0Security HTTP headersserver.js
cors^2.8.5Cross-origin resource sharingserver.js
express-rate-limit^7.1.5API + auth rate limitingserver.js
express-validator^7.0.1Request input validationroutes
multer^1.4.5-lts.1File upload handling (receipts, photos)expenses.js, gps.js
nodemailer^6.9.7SMTP email sendingemailService.js
pdfkit^0.14.0PDF payslip generationpayslipPDF.js
razorpay^2.9.2Payment gateway for SaaS billingbilling.js
winston^3.11.0Structured logginglogger.js
sanitize-html^2.11.0XSS prevention, input sanitizationsanitize.js
compression^1.7.4Gzip response compressionserver.js
uuid^9.0.1UUID generationvarious routes
morgan^1.10.0HTTP request loggingserver.js
dotenv^16.3.1Environment variable loadingserver.js
Complete Project File Structure
Backend β€” 25 Route Files
backend/src/ β”œβ”€β”€ server.js Express app, middleware, 25+ routes β”œβ”€β”€ config/ β”‚ β”œβ”€β”€ database.js PostgreSQL pool connection β”‚ └── logger.js Winston structured logging β”œβ”€β”€ middleware/ β”‚ β”œβ”€β”€ auth.js JWT verify + tenant_id from DB β”‚ β”œβ”€β”€ rbac.js Role-based access control β”‚ β”œβ”€β”€ rateLimiter.js 500/15min API, 20/15min auth β”‚ β”œβ”€β”€ sanitize.js XSS input sanitization β”‚ └── tenantMiddleware.js Plan limit check β”œβ”€β”€ routes/ β”‚ β”œβ”€β”€ auth.js Login, OTP, refresh token β”‚ β”œβ”€β”€ employees.js Employee CRUD + salary structure β”‚ β”œβ”€β”€ attendance.js Daily attendance + GPS β”‚ β”œβ”€β”€ payroll.js calcPayroll engine + PDF β”‚ β”œβ”€β”€ leaves.js Apply, approve, balance β”‚ β”œβ”€β”€ expenses.js Claims + receipt upload β”‚ β”œβ”€β”€ compliance.js PF/ESI/TDS/PT reports β”‚ β”œβ”€β”€ analytics.js 6 parallel DB queries KPIs β”‚ β”œβ”€β”€ ats.js 7-stage recruitment Kanban β”‚ β”œβ”€β”€ billing.js Razorpay + webhook HMAC β”‚ β”œβ”€β”€ gpsAttendance.js Haversine geofence check β”‚ β”œβ”€β”€ admin.js Users + per-user permissions β”‚ β”œβ”€β”€ superAdmin.js All tenants β€” no tenant filter β”‚ β”œβ”€β”€ shifts.js Shift master + bulk assign β”‚ β”œβ”€β”€ advances.js Salary advance + EMI β”‚ └── ... 10 more routes β”œβ”€β”€ services/ β”‚ β”œβ”€β”€ payslipPDF.js PDFKit payslip generator β”‚ β”œβ”€β”€ emailService.jsNodemailer SMTP dispatch β”‚ └── emailTemplates.js HTML email templates └── utils/ └── geoUtils.js Haversine formula GPS
Frontend β€” 33 Page Components
frontend/src/ β”œβ”€β”€ App.jsx Router + auth guard + role routing β”œβ”€β”€ pages/ β”‚ β”œβ”€β”€ Login.jsx JWT login + tenant check β”‚ β”œβ”€β”€ Dashboard.jsx Role-based dashboard switch β”‚ β”œβ”€β”€ Employees.jsx List + search + filter β”‚ β”œβ”€β”€ EmployeeForm.jsx 5-step create/edit form β”‚ β”œβ”€β”€ Attendance.jsx Table + GPS status β”‚ β”œβ”€β”€ Payroll.jsx Run + preview + PDF β”‚ β”œβ”€β”€ Leaves.jsx Apply + approve + balance β”‚ β”œβ”€β”€ Expenses.jsx Claims + receipt upload β”‚ β”œβ”€β”€ ATSKanban.jsx 7-stage Kanban board β”‚ β”œβ”€β”€ ComplianceReports.jsx PF/ESI/TDS β”‚ β”œβ”€β”€ AnalyticsDashboard.jsx Charts + KPIs β”‚ β”œβ”€β”€ AuditLogs.jsx Activity trail β”‚ β”œβ”€β”€ AdminUsers.jsx RBAC + permissions β”‚ β”œβ”€β”€ BillingPortal.jsx Razorpay plans β”‚ β”œβ”€β”€ Settings.jsx Company + GPS + SMTP β”‚ └── ... 18 more pages β”œβ”€β”€ dashboards/ β”‚ β”œβ”€β”€ AdminDashboard.jsx β”‚ β”œβ”€β”€ HRDashboard.jsx β”‚ β”œβ”€β”€ ManagerDashboard.jsx β”‚ β”œβ”€β”€ EmployeeDashboard.jsx β”‚ └── SuperAdminDashboard.jsx β”œβ”€β”€ components/ β”‚ β”œβ”€β”€ GPSCheckin.jsx Live map + geofence UI β”‚ └── layout/Layout.jsx Sidebar + topbar β”œβ”€β”€ services/ authService, employeeService... β”œβ”€β”€ context/ β”‚ └── AuthContext.jsJWT + user state global └── utils/ β”œβ”€β”€ api.js Axios instance + interceptors └── useGPS.js Browser geolocation hook
Database β€” 13 Migration Phases
database/ β”œβ”€β”€ init.sql Base schema: employees, users, departments... β”œβ”€β”€ migration_phase7.sql ATS + recruitment pipeline β”œβ”€β”€ migration_phase8.sql GPS + geofence tables β”œβ”€β”€ migration_phase9.sql Tenant isolation + user_permissions β”œβ”€β”€ migration_phase9b.sql tenant_features JSON column β”œβ”€β”€ migration_phase10.sql Billing + subscription plans β”œβ”€β”€ migration_phase11.sql White label config β”œβ”€β”€ migration_phase12.sql Audit logs old_data/new_data β”œβ”€β”€ migration_phase13.sql Analytics snapshots β”œβ”€β”€ Master migration.sql Combined all phases └── run_migrations.sh Smart β€” skips already applied
5 GitHub Actions Workflow Files
πŸš€
ci-cd.yml
Trigger: push to main (~3 min)
Lint β€” ESLint backend code
Security β€” Trivy + Gitleaks + npm audit
Tests β€” Jest + PostgreSQL + Redis services
Docker Build + Push to Hub
SSH Deploy to EC2 + migrations
Smoke Test β€” curl /health
πŸ”
pr-checks.yml
Trigger: Pull Request to main
Validate PR title (skip Dependabot)
Build test β€” npm install check
Docker build test (no push)
πŸ”’
security-scan.yml
Trigger: Every Monday 2AM
npm audit β€” critical vulnerabilities
Trivy β€” filesystem scan
Gitleaks β€” secret detection
↩️
rollback.yml
Trigger: Manual only
Input: specific image tag to rollback to
SSH to EC2 β†’ docker compose pull tag
Restart containers with old image
πŸ’Ύ
backup.yml
Trigger: Every day 1AM
SSH to EC2
pg_dump β†’ compressed backup file
Store on EC2 with date stamp
Complete CI/CD Pipeline Flow
git push main
β†’
GitHub Actions trigger
β†’
Lint (38s)
+
Security (62s)
β†’
Tests (60s)
β†’
Docker Build + Push (26s)
β†’
Deploy EC2 (40s)
β†’
Smoke Test βœ…
8 GitHub Secrets Required
πŸ”‘
DOCKERHUB_USERNAME
Docker Hub login
πŸ”‘
DOCKERHUB_TOKEN
Hub access token
πŸ”‘
EC2_HOST
YOUR_EC2_IP
πŸ”‘
EC2_SSH_KEY
your-server-key.pem content
πŸ”’
DB_PASSWORD
PostgreSQL password
πŸ”’
REDIS_PASSWORD
Redis password
πŸ”’
JWT_SECRET
128-char random string
πŸ”’
REFRESH_TOKEN_SECRET
128-char random string
Deploy Script β€” EC2 SSH Commands
# Runs on EC2 via appleboy/ssh-action set -e cd ~/hr-admin-payroll-erp # 1. Pull latest code git fetch origin main git reset --hard origin/main # 2. Pull new Docker images from Hub docker compose pull # 3. Restart containers (zero-downtime) docker compose up -d --remove-orphans --no-build # 4. Run DB migrations (smart β€” skips done) bash database/run_migrations.sh # 5. Health check loop (60s max) for i in $(seq 1 12); do if curl -sf http://localhost:5000/health; then echo "Backend healthy!" break fi # Rollback if all 12 checks fail if [ $i -eq 12 ]; then docker compose down git reset --hard HEAD~1 docker compose up -d exit 1 fi sleep 5 done # 6. Cleanup old images docker image prune -f
Docker Images β€” Built and Pushed to Hub
Backend Dockerfile β€” node:20-alpine
FROM node:20-alpine WORKDIR /app # Install deps (cache layer) COPY package*.json ./ RUN npm install --only=production --silent COPY . . RUN mkdir -p uploads logs # Non-root user for security RUN addgroup -g 1001 -S nodejs RUN adduser -S nodejs -u 1001 RUN chown -R nodejs:nodejs /app USER nodejs EXPOSE 5000 CMD ["node", "src/server.js"] # Tags pushed: your-dockerhub/hr-erp-backend:latest your-dockerhub/hr-erp-backend:abc12345
Frontend Dockerfile β€” Multi-stage Build
# Stage 1: Build React app FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm install --silent COPY . . ARG REACT_APP_API_URL=/api ENV REACT_APP_API_URL=$REACT_APP_API_URL # Fix Node 20 + OpenSSL issue ENV NODE_OPTIONS=--openssl-legacy-provider RUN npm run build # Stage 2: Serve with Nginx FROM nginx:1.25-alpine COPY --from=builder /app/build /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] # Tags pushed: your-dockerhub/hr-erp-frontend:latest
EC2 Instance Configuration
Instance Specs
Namehr-erp-server
AMIUbuntu Server 22.04 LTS
Instance Typet3.medium
vCPU2
RAM4 GB
Storage25 GB gp3
Public IPYOUR_EC2_IP
Key Pairyour-server-key.pem
Security Group β€” Firewall Rules
TypePortSource
SSH22My IP only
HTTP80Anywhere
HTTPS443Anywhere
3000 (React)3000127.0.0.1 only
5000 (API)5000127.0.0.1 only
5432 (PG)5432127.0.0.1 only
6379 (Redis)6379127.0.0.1 only
Software Installed on EC2
Docker EngineInstalled
NginxInstalled
CertbotInstalled
python3-certbot-nginxInstalled
UFW FirewallEnabled
systemd servicehr-erp.service
Docker groupubuntu user added
Nginx Config β€” Reverse Proxy + SSL
# /etc/nginx/sites-available/hrerp.infikuber.com # HTTP β†’ HTTPS redirect (added by Certbot) server { listen 80; server_name hrerp.infikuber.com; return 301 https://$host$request_uri; } server { listen 443 ssl; server_name hrerp.infikuber.com; # SSL β€” Let's Encrypt via Certbot ssl_certificate /etc/letsencrypt/live/hrerp.../fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/hrerp.../privkey.pem; # Frontend (React) β€” port 3000 location / { proxy_pass http://localhost:3000; } # Backend API β€” port 5000 location /api/ { proxy_pass http://localhost:5000/api/; } }
# systemd auto-start service # /etc/systemd/system/hr-erp.service [Unit] Description=HR ERP Docker Compose After=docker.service Requires=docker.service [Service] Type=oneshot RemainAfterExit=yes WorkingDirectory=/home/ubuntu/hr-admin-payroll-erp ExecStart=/usr/bin/docker compose up -d ExecStop=/usr/bin/docker compose down User=ubuntu [Install] WantedBy=multi-user.target # Commands: sudo systemctl daemon-reload sudo systemctl enable hr-erp sudo systemctl start hr-erp # DNS β€” Hostinger A Record Type: A Name: hrerp Points to: YOUR_EC2_IP TTL: 14400 # SSL Certbot command sudo certbot --nginx -d hrerp.infikuber.com
4 Docker Containers β€” hr_net Network
healthy
hr_erp_db
postgres:15-alpine
127.0.0.1:5432
Database: hr_erp Β· User: hr_admin Β· Data volume: postgres_data Β· Init: init.sql
healthy
hr_erp_redis
redis:7-alpine
127.0.0.1:6379
256MB max Β· LRU eviction Β· Persistence: save 60 1 Β· Password protected
healthy
hr_erp_api
hr-erp-backend:latest
127.0.0.1:5000
Node.js 20 Β· Depends on: postgres + redis healthy Β· Volumes: uploads, logs
running
hr_erp_ui
hr-erp-frontend:latest
127.0.0.1:3000
Nginx serving React build Β· Depends on: backend healthy Β· Port 80 internal
docker-compose.yml β€” Key Configuration
# docker-compose.yml β€” All 4 containers services: postgres: image: postgres:15-alpine container_name: hr_erp_db environment: POSTGRES_DB: hr_erp POSTGRES_PASSWORD: ${DB_PASSWORD:-Hr@Secret123} volumes: - postgres_data:/var/lib/postgresql/data - ./database/init.sql:/docker-entrypoint-initdb.d/init.sql:ro ports: ["127.0.0.1:5432:5432"] ← localhost only, not public healthcheck: { test: pg_isready, interval: 10s, retries: 5 } redis: image: redis:7-alpine command: redis-server --requirepass ${REDIS_PASSWORD} --maxmemory 256mb backend: image: ${DOCKERHUB_USERNAME}/hr-erp-backend:latest environment: DATABASE_URL: postgresql://hr_admin:${DB_PASSWORD}@postgres:5432/hr_erp REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379 JWT_SECRET: ${JWT_SECRET} depends_on: postgres: { condition: service_healthy } redis: { condition: service_healthy } healthcheck: ← Uses node http module, NOT wget (Alpine has no wget) test: node -e "require('http').get('http://localhost:5000/health',...)" frontend: image: ${DOCKERHUB_USERNAME}/hr-erp-frontend:latest ports: ["127.0.0.1:3000:80"] depends_on: backend: { condition: service_healthy } networks: hr_net: driver: bridge ipam: { config: [{ subnet: 172.20.0.0/16 }] }
ERROR 1
Resource not accessible by integration
pr-checks.yml β†’ Validate PR job
Dependabot auto-creates PRs to update npm packages. PR Checks workflow tried to write a comment β†’ bot PRs don't have write permission.
βœ… Fix
jobs: validate: if: github.actor != 'dependabot[bot]'
ERROR 2
Unable to cache dependencies
CI/CD β†’ Lint job
GitHub Actions tried to cache npm packages. Cache requires package-lock.json which was in .gitignore β€” didn't exist.
βœ… Fix
Removed cache config from setup-node step completely. Changed npm ci β†’ npm install in Dockerfiles.
ERROR 3
YAML syntax error on line 44
ci-cd.yml β€” GitHub showed red X
Used Unicode box-drawing characters ╔══╗ as visual separators in YAML comments. YAML parser rejected the file entirely.
βœ… Fix
Replaced entire ci-cd.yml with clean version using only plain ASCII characters in comments.
ERROR 4
limit_req_zone not allowed here
EC2 β†’ sudo nginx -t
Rate limit zone defined INSIDE server{} block. Nginx rule: limit_req_zone must be in http{} block only.
βœ… Fix
Removed limit_req_zone completely. Used Express's own rate limiter (express-rate-limit) instead.
ERROR 5
No ssl_certificate defined
EC2 β†’ sudo nginx -t
Nginx config had SSL certificate paths but Certbot hadn't run yet β€” certificate files didn't exist on disk.
βœ… Fix
Commented out SSL lines temporarily. Got HTTP working first, then ran Certbot which auto-added SSL config.
ERROR 6
hr-erp.service does not exist
EC2 β†’ sudo systemctl enable hr-erp
Tried to enable auto-start service without creating the .service file first. systemctl needs the file in /etc/systemd/system/.
βœ… Fix
sudo tee /etc/systemd/system/hr-erp.service sudo systemctl daemon-reload sudo systemctl enable hr-erp
ERROR 7
Username and password required (Docker Hub)
GitHub Actions β†’ Docker Build & Push
Workflow tried to push Docker images to Docker Hub but DOCKERHUB_USERNAME and DOCKERHUB_TOKEN secrets were not added to GitHub yet.
βœ… Fix
hub.docker.com β†’ Security β†’ New Access Token. Added DOCKERHUB_USERNAME + DOCKERHUB_TOKEN to GitHub Secrets.
ERROR 8
npm ci failed, exit code 1
GitHub Actions β†’ Docker Build
Both Dockerfiles used npm ci which REQUIRES package-lock.json. Repo didn't have it.
βœ… Fix
# Before RUN npm ci --only=production --silent # After RUN npm install --only=production --silent
ERROR 9
Could not read Username for github.com
GitHub Actions β†’ Deploy via SSH
Deploy script runs git fetch on EC2. EC2 had cloned repo via HTTPS which needs username/password. No credentials stored.
βœ… Fix
ssh-keygen -t ed25519 -C "ec2-deploy" # Added public key to GitHub SSH keys rm -rf ~/hr-admin-payroll-erp git clone [email protected]:your-github/hr-admin-payroll-erp
ERROR 10
.env key cannot contain a space
EC2 β†’ docker compose up -d
Accidentally typed "chmod 600 .env" as text inside the .env file. Docker read it as a key=value pair β€” spaces invalid.
βœ… Fix
Opened nano .env, deleted the wrong first line. Correct format: KEY=value with NO spaces in key names.
ERROR 11
Port 5432 already in use
EC2 β†’ docker compose up -d
Ubuntu had system PostgreSQL installed and running on port 5432. Docker also tried to use 5432. Two processes can't share one port.
βœ… Fix
sudo systemctl stop postgresql sudo systemctl disable postgresql docker compose up -d
ERROR 12
init.sql: Is a directory
EC2 β†’ docker logs hr_erp_db
When creating database/init.sql on GitHub, GitHub created a FOLDER instead of a FILE. Docker tried to mount directory as file β†’ PostgreSQL couldn't initialize schema.
βœ… Fix
sudo rm -rf database/init.sql git pull # Get correct file
ERROR 13
Backend container unhealthy
EC2 β†’ docker compose ps
Healthcheck used wget but node:20-alpine doesn't include wget. Container marked unhealthy β†’ frontend wouldn't start.
βœ… Fix β€” Use Node.js built-in http module
test: node -e "require('http').get( 'http://localhost:5000/health', r=>process.exit(r.statusCode===200?0:1) )"
ERROR 14
Certbot: Invalid response 404
EC2 β†’ sudo certbot --nginx
nslookup showed 3 IPs: EC2 (correct) + Hostinger old A record + IPv6 record. Let's Encrypt hit wrong IP β†’ 404 β†’ SSL verification failed.
βœ… Fix
Hostinger DNS β†’ Deleted old A record (OLD_HOSTINGER_IP) and AAAA record. Kept only EC2 A record. Waited 10 min, re-ran certbot.
https://hrerp.infikuber.com
HR Admin & Payroll ERP Β· Multi-tenant SaaS Β· All containers healthy
Login
[email protected]
**********
GitHub Actions β€” All Jobs Green βœ…
JobTimeStatusWhat it does
Lint38sPassedESLint on all backend JS files
Security Scan1m 2sPassedTrivy + Gitleaks + npm audit
Tests1m 0sPassedJest unit tests with live PostgreSQL + Redis
Docker Build & Push26sPassedBackend + Frontend images pushed to Docker Hub
Deploy to Production40sPassedSSH β†’ git pull β†’ docker compose up β†’ migrations
Total Pipeline~3 minAll GreenFrom git push to live production
Live Docker Containers Status
healthy
hr_erp_db
postgres:15-alpine
127.0.0.1:5432
healthy
hr_erp_redis
redis:7-alpine
127.0.0.1:6379
healthy
hr_erp_api
hr-erp-backend:latest
127.0.0.1:5000
running
hr_erp_ui
hr-erp-frontend:latest
127.0.0.1:3000
10 Key Lessons Learned
Lesson 1 β€” npm
Use npm install, not npm ci
npm ci requires package-lock.json. If you don't commit it, use npm install everywhere β€” Dockerfiles, GitHub Actions, local.
Lesson 2 β€” Docker Alpine
Alpine has no wget or curl
node:20-alpine is minimal. Ne skip Dependabot bot PRs
Add if: github.actor != 'dependabot[bot]' to any job that needs write permission. Dependabot PRs have limited permissions.
Lesson 4 β€” YAML
Never use Unicode in YAML files
Box-drawing characters ╔══╗ break the YAML parser. Use only plain ASCII in .yml files. GitHub Actions will reject the file silently.
Lesson 5 β€” DNS
Check ALL DNS records (A + AAAA)
Multiple DNS records cause SSL verification to fail. Delete old Hostinger A and AAAA records. Keep only EC2 A record. Wait 10 min before certbot.
Lesson 6 β€” Git on Server
Always SSH clone, never HTTPS
HTTPS git needs credentials. SSH uses key pair. Generate ed25519 key on EC2, add public key to GitHub SSH Keys, then clone via SSH.
Lesson 7 β€” Docker Ports
Bind to 127.0.0.1, not 0.0.0.0
All container ports should bind to 127.0.0.1 (localhost only). Let Nginx be the only public entry point on 80/443.
Lesson 8 β€” .env Files
No spaces in .env key names
chmod 600 .env to protect it. Never commit .env to git. No spaces in key names. Format is always KEY=value.
Lesson 9 β€” PostgreSQL
Disable system PostgreSQL on Ubuntu
Ubuntu has PostgreSQL in apt that starts on port 5432. Disable it before Docker uses the same port: systemctl stop + disable postgresql.
Lesson 10 β€” Nginx
HTTP first, then SSL
Always get HTTP working first with sudo nginx -t. Then run Certbot β€” it auto-adds SSL config. Never add SSL config before certs exist.